{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> MCP（Model Context Protocol, 模型上下文协议）是 AI 应用程序和 Agents 连接并使用数据源（例如本地文件、数据库或内容存储库）和工具（例如 GitHub、Google Maps 或 Puppeteer）的标准方式。\n",
    "\n",
    "MCP 定义了 AI Agents 如何与应用程序通信的新兴标准。那么使用 FastAPI 构建的应用程序如何与 MCP 兼容呢？很幸运，FastAPI 社区已经为 MCP 提供了一个扩展库：[`fastapi-mcp`](https://github.com/tadata-org/fastapi_mcp)。那么 `fastapi-mcp` 有哪些功能呢？\n",
    "\n",
    "- 内置身份认证：保证只有授权的 AI 可以使用你的 API\n",
    "- 零配置使用：只需几行代码即可接入\n",
    "- 灵活部署：可以和你的主应用一起运行，也可以单独部署\n",
    "- 高效通信：直接使用 FastAPI 内部的ASGI 传输，而无需额外的HTTP请求\n",
    "\n",
    "## 安装\n",
    "\n",
    "我们基于 `第01课-路径参数.md` 的代码进行修改，在 Jupyter notebook 中安装 `fastapi-mcp` 扩展库。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install fastapi-mcp"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 基本用法\n",
    "\n",
    "使用 `fastapi-mcp` 时，你可以通过创建一个 `FastApiMCP` 实例并将其挂载到你的 FastAPI 应用上来启用 MCP。\n",
    "\n",
    "例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Started server process [21903]\n",
      "INFO:     Waiting for application startup.\n",
      "INFO:     Application startup complete.\n",
      "INFO:     Uvicorn running on http://0.0.0.0:8009 (Press CTRL+C to quit)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:50855 - \"GET /mcp HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:60177 - \"GET /mcp HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:60188 - \"POST /mcp/messages/?session_id=5f3300f94bd640889e9718a3d73a4080 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:60189 - \"POST /mcp/messages/?session_id=5f3300f94bd640889e9718a3d73a4080 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:60190 - \"POST /mcp/messages/?session_id=5f3300f94bd640889e9718a3d73a4080 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:61125 - \"GET /mcp HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:61126 - \"POST /mcp/messages/?session_id=5acfaf3b1dae478d9bebc5c36554f684 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:61127 - \"POST /mcp/messages/?session_id=5acfaf3b1dae478d9bebc5c36554f684 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:61130 - \"POST /mcp/messages/?session_id=5acfaf3b1dae478d9bebc5c36554f684 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:62612 - \"GET /mcp HTTP/1.1\" 200 OK\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Shutting down\n",
      "INFO:     Waiting for connections to close. (CTRL+C to force quit)\n"
     ]
    }
   ],
   "source": [
    "import uvicorn\n",
    "from fastapi import FastAPI\n",
    "\n",
    "# 导入 fastapi-mcp 扩展库\n",
    "from fastapi_mcp import FastApiMCP\n",
    "\n",
    "app = FastAPI()\n",
    "\n",
    "# 基于原有的 app 创建 MCP 服务\n",
    "mcp = FastApiMCP(app)\n",
    "# 将 MCP 服务器直接安装到应用程序中\n",
    "mcp.mount()\n",
    "\n",
    "@app.get(\"/\")\n",
    "async def root():\n",
    "    return {\"message\": \"Hello World\"}\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    config = uvicorn.Config(app, host='0.0.0.0', port=8009)\n",
    "    server = uvicorn.Server(config)\n",
    "    await server.serve()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "点 ctrl+enter 运行这个格子。\n",
    "控制台输出以下信息：\n",
    "\n",
    "```log\n",
    "INFO:     Started server process [20419]\n",
    "INFO:     Waiting for application startup.\n",
    "INFO:     Application startup complete.\n",
    "INFO:     Uvicorn running on http://0.0.0.0:8009 (Press CTRL+C to quit)\n",
    "INFO:     127.0.0.1:54543 - \"GET /docs HTTP/1.1\" 200 OK\n",
    "INFO:     127.0.0.1:54543 - \"GET /openapi.json HTTP/1.1\" 200 OK\n",
    "INFO:     127.0.0.1:54688 - \"GET /mcp HTTP/1.1\" 200 OK\n",
    "INFO:     Shutting down\n",
    "INFO:     Waiting for connections to close. (CTRL+C to force quit)\n",
    "INFO:     Finished server process [20419]\n",
    "```\n",
    "\n",
    "访问 http://127.0.0.1:8009/mcp 可以看到 MCP 服务器的运行情况。如：\n",
    "\n",
    "```log\n",
    "event: endpoint\n",
    "data: /mcp/messages/?session_id=47bbb4564ea746d3bb1ffc063985eb19\n",
    "\n",
    ": ping - 2025-05-08 01:36:20.203345+00:00\n",
    "\n",
    ": ping - 2025-05-08 01:36:35.204371+00:00\n",
    "\n",
    ": ping - 2025-05-08 01:36:50.206171+00:00\n",
    "\n",
    ": ping - 2025-05-08 01:37:05.208072+00:00\n",
    "```\n",
    "\n",
    "在 MCP Client 中如何使用呢？对于任何支持 SSE 的 MCP 客户端，我们只需提供 MCP url，也就是 http://127.0.0.1:8009/mcp，就可以开始工作了。\n",
    "所有最流行的 MCP 客户端（Claude Desktop、Cursor 和 Windsurf 等）都使用以下配置格式：\n",
    "\n",
    "```json\n",
    "{\n",
    "  \"mcpServers\": {\n",
    "    \"fastapi-mcp\": {\n",
    "      \"url\": \"http://127.0.0.1:8009/mcp\"\n",
    "    }\n",
    "  }\n",
    "}\n",
    "```\n",
    "\n",
    "或者：\n",
    "\n",
    "```json\n",
    "{\n",
    "  \"mcpServers\": {\n",
    "    \"fastapi-mcp\": {\n",
    "      \"command\": \"npx\",\n",
    "      \"args\": [\n",
    "        \"mcp-remote\",\n",
    "        \"http://127.0.0.1:8009/mcp\",\n",
    "        \"8080\"  // 可选端口号。如果你想要 OAuth 正常工作且没有动态客户端注册功能，这个端口号是必需的。\n",
    "      ]\n",
    "    }\n",
    "  }\n",
    "}\n",
    "\n",
    "配置完成之后我们就能在 MCP 客户端中使用 `fastapi-mcp` 提供的服务了。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 身份认证\n",
    "\n",
    "`fastapi-mcp` 支持使用现有的 FastAPI 依赖项进行身份验证和授权，也支持 OAuth 2、自定义 OAuth 元数据、Auth0 等。此处以最基础的 Authorization 为例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Started server process [76868]\n",
      "INFO:     Waiting for application startup.\n",
      "INFO:     Application startup complete.\n",
      "INFO:     Uvicorn running on http://0.0.0.0:8009 (Press CTRL+C to quit)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:     127.0.0.1:61294 - \"GET /mcp HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:62357 - \"GET /mcp HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:62357 - \"GET /favicon.ico HTTP/1.1\" 404 Not Found\n",
      "INFO:     127.0.0.1:62648 - \"GET /private HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:62759 - \"GET /private HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:62818 - \"GET / HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:63783 - \"GET / HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:64444 - \"GET /private HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:50093 - \"GET /mcp HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:50169 - \"GET /mcp HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:50169 - \"GET /mcp HTTP/1.1\" 403 Forbidden\n",
      "INFO:     127.0.0.1:50380 - \"GET /mcp HTTP/1.1\" 200 OK\n",
      "INFO:     127.0.0.1:50381 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50381 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50381 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50382 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50383 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50381 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50381 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50382 - \"POST /mcp/messages/?session_id=25fb35e6c448446692fb87bbac708722 HTTP/1.1\" 202 Accepted\n",
      "INFO:     127.0.0.1:50562 - \"GET /mcp HTTP/1.1\" 403 Forbidden\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:     Shutting down\n",
      "INFO:     Waiting for connections to close. (CTRL+C to force quit)\n"
     ]
    }
   ],
   "source": [
    "import uvicorn\n",
    "from fastapi import FastAPI, Depends\n",
    "from fastapi.security import HTTPBearer\n",
    "from fastapi_mcp import FastApiMCP, AuthConfig\n",
    "\n",
    "app = FastAPI()\n",
    "\n",
    "\n",
    "@app.get(\"/\")\n",
    "async def root():\n",
    "    return {\"message\": \"Hello World\"}\n",
    "\n",
    "token_auth_scheme = HTTPBearer()\n",
    "@app.get(\"/private\")\n",
    "async def private(token = Depends(token_auth_scheme)):\n",
    "    return token.credentials\n",
    "\n",
    "mcp = FastApiMCP(\n",
    "    app,\n",
    "    name=\"Protected MCP\",\n",
    "    auth_config=AuthConfig(\n",
    "        dependencies=[Depends(token_auth_scheme)],\n",
    "    ),\n",
    ")\n",
    "mcp.mount()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    config = uvicorn.Config(app, host='0.0.0.0', port=8009)\n",
    "    server = uvicorn.Server(config)\n",
    "    await server.serve()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "此时我们访问 http://127.0.0.1:8009/mcp 时，会发现无法直接访问，提示需要认证： `{\"detail\":\"Not authenticated\"}`。这时在 MCP 客户端中输入认证信息，即可访问，如：\n",
    "\n",
    "```json\n",
    "{\n",
    "  \"mcpServers\": {\n",
    "    \"remote-example\": {\n",
    "      \"command\": \"npx\",\n",
    "      \"args\": [\n",
    "        \"mcp-remote\",\n",
    "        \"http://localhost:8000/mcp\",\n",
    "        \"--header\",\n",
    "        \"Authorization:${AUTH_HEADER}\"\n",
    "      ]\n",
    "    },\n",
    "    \"env\": {\n",
    "      \"AUTH_HEADER\": \"Bearer <your-token>\"\n",
    "    }\n",
    "  }\n",
    "}\n",
    "```\n",
    "\n",
    "像 Cherry Studio 这样的 MCP 客户端，则需在请求头中输入认证信息，如：\n",
    "\n",
    "```txt\n",
    "Authorization=Bearer ${AUTH_HEADER}\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 单独部署\n",
    "\n",
    "正如前面提到的特性中， `fastapi-mcp` 可以和你的主应用一起运行，也可以单独部署。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Overwriting main.py\n"
     ]
    }
   ],
   "source": [
    "%%writefile main.py\n",
    "from fastapi import FastAPI\n",
    "from fastapi_mcp import FastApiMCP\n",
    "\n",
    "app = FastAPI(title=\"主应用\")\n",
    "\n",
    "# 单独部署 MCP 服务\n",
    "mcp_app = FastAPI(title=\"MCP 服务\")\n",
    "mcp = FastApiMCP(app)\n",
    "mcp.mount(mcp_app)\n",
    "\n",
    "@app.get(\"/\")\n",
    "async def root():\n",
    "    return {\"message\": \"Hello World\"}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[32mINFO\u001b[0m:     Started server process [\u001b[36m76758\u001b[0m]\n",
      "\u001b[32mINFO\u001b[0m:     Waiting for application startup.\n",
      "\u001b[32mINFO\u001b[0m:     Application startup complete.\n",
      "\u001b[32mINFO\u001b[0m:     Uvicorn running on \u001b[1mhttp://0.0.0.0:8000\u001b[0m (Press CTRL+C to quit)\n",
      "\u001b[32mINFO\u001b[0m:     127.0.0.1:56272 - \"\u001b[1mGET /mcp HTTP/1.1\u001b[0m\" \u001b[31m404 Not Found\u001b[0m\n",
      "^C\n",
      "\u001b[32mINFO\u001b[0m:     Shutting down\n",
      "\u001b[32mINFO\u001b[0m:     Waiting for application shutdown.\n",
      "\u001b[32mINFO\u001b[0m:     Application shutdown complete.\n",
      "\u001b[32mINFO\u001b[0m:     Finished server process [\u001b[36m76758\u001b[0m]\n",
      "\u001b[32mINFO\u001b[0m:     Started server process [\u001b[36m76818\u001b[0m]\n",
      "\u001b[32mINFO\u001b[0m:     Waiting for application startup.\n",
      "\u001b[32mINFO\u001b[0m:     Application startup complete.\n",
      "\u001b[32mINFO\u001b[0m:     Uvicorn running on \u001b[1mhttp://0.0.0.0:8009\u001b[0m (Press CTRL+C to quit)\n",
      "\u001b[32mINFO\u001b[0m:     127.0.0.1:56422 - \"\u001b[1mGET /mcp HTTP/1.1\u001b[0m\" \u001b[32m200 OK\u001b[0m\n",
      "\u001b[32mINFO\u001b[0m:     127.0.0.1:56425 - \"\u001b[1mGET /mcp HTTP/1.1\u001b[0m\" \u001b[32m200 OK\u001b[0m\n"
     ]
    }
   ],
   "source": [
    "!uvicorn main:app --host 0.0.0.0 --port 8000 \n",
    "!uvicorn main:mcp_app --host 0.0.0.0 --port 8009"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 基本配置\n",
    "\n",
    "### `operation_id` 定义 MCP tool 名称\n",
    "\n",
    "在前面的示例中，我们并没有为接口指定 MCP tool 名称，但 MCP 客户端也能够发现，比如 `root_get`、 `private_private_get` 等，这是因为 `fastapi-mcp` 中使用了 FastAPI 自动生成的 `operation_id`, 命名规则是 `{HTTP方法}_{路径层级}_{变量名}__{变量名}__{方法}` 。如果你想自定义 MCP tool 名称，可以使用 `operation_id`，如：\n",
    "\n",
    "```python\n",
    "@app.get(\"/private\", tags=[\"auth\"] , operation_id=\"private\")\n",
    "async def private(token = Depends(token_auth_scheme)):\n",
    "    return token.credentials\n",
    "```\n",
    "\n",
    "此时，MCP 客户端中看到的 tool 名称原来是 `private_private_get`，而现在就是 `private` 了。\n",
    "\n",
    "### 自定义公开的 tool\n",
    "\n",
    "`fastapi-mcp` 默认会公开所有接口作为 MCP tool，如果你想要隐藏一些接口不作为 tool 暴露，可以使用 `exclude_operations` 参数，如：\n",
    "\n",
    "```python\n",
    "# 排除特定接口\n",
    "mcp = FastApiMCP(\n",
    "    app,\n",
    "    exclude_operations=[\"private\"]\n",
    ")\n",
    "```\n",
    "\n",
    "此时，MCP 客户端中就不会看到 `private` 这个 tool 了。\n",
    "也可以通过 `include_operations` 参数来指定哪些接口作为 tool 暴露，如：\n",
    "\n",
    "```python\n",
    "# 只暴露特定接口\n",
    "mcp = FastApiMCP(\n",
    "    app,\n",
    "    include_operations=[\"private\"]\n",
    ")\n",
    "```\n",
    "\n",
    "此时，MCP 客户端中就只会看到 `private` 这个 tool 了。\n",
    "\n",
    "除了 `exclude_operations` 和 `include_operations` 之外，你还可以使用 `exclude_tags` 和 `include_tags` 来指定哪些接口作为 tool 暴露，如：\n",
    "\n",
    "```python\n",
    "# 排除特定 tag\n",
    "mcp = FastApiMCP(\n",
    "    app,\n",
    "    exclude_tags=[\"auth\"] # 需先给接口添加 tags 参数\n",
    ")\n",
    "```\n",
    "\n",
    "此时，MCP 客户端中就不会看到 `auth` 这个 tag 下的 tool 了。\n",
    "\n",
    "## 总结\n",
    "\n",
    "`fastapi-mcp` 是一个非常实用的 FastAPI 扩展，它能够将你的 API 作为 MCP 服务暴露出来，使得你可以在各种支持 MCP 的客户端中使用它们。通过简单的配置和少量的代码修改，你就可以轻松地将现有的 FastAPI 应用转换为 MCP 服务，从而提升开发效率和工作流。\n",
    "\n",
    "希望这篇教程能够帮助你更好地理解和使用 `fastapi-mcp`！如果你有任何问题或建议，欢迎随时提出。\n",
    "后面的课程我们会使用 `fastapi-mcp` 来构建一个 天气查询 MCP 服务。\n",
    "\n",
    "更多 MCP 相关的内容，可以期待 DataWhale 的后续课程。或者参考以下资源:\n",
    "\n",
    "- [MCP 官方文档](https://modelcontextprotocol.io/)\n",
    "- [MCP 中文社区](https://mcpservers.cn/)\n",
    "- [mcp.so](https://mcp.so/)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
